my $DEBUG++;
my $LOG_FILENAME;

my $runTime = getRunTime();
my %EscapeChar = ('"','\\', '\'','\'', '/', '\\' );

my $sourceDir = $ARGV[0] || ".";
$sourceDir =~ s/\\/\//g;
$sourceDir =~ s/\/$//;

if (!-e $sourceDir) { print "Source directory [$sourceDir] not found\n"; exit(); }

my $targetDir = $ARGV[1];
if (!$targetDir) {
	$targetDir = $sourceDir;
	$targetDir = "bundle-convertor_$runTime.out" if $DEBUG;
} else {
	$targetDir =~ s/\\/\//g;
	$targetDir =~ s/\/$//;
}
if (-f $targetDir) {
	$targetDir =~ s/\/[^\/]+$//;
}
checkDir($targetDir);

my @Errors = ();
my $processed = 0;
init();
if (-d $sourceDir) {
	migrateDir($sourceDir,$targetDir);
	updateBundleMenu($targetDir);
} else {
	if (-f $sourceDir) {
		$targetDir .= "/".$sourceDir;
		migrateBundle($sourceDir);
	}
}
print "\n".scalar(@Errors)." errors in $processed processed.\n";
logit(scalar(@Errors)." errors in $processed processed.\n");
destroy();

sub migrateDir {
	my $path = shift;
	opendir (DIR, $path) or die "Unable to open $path: $!";
	my @files = map { $path . '/' . $_ } grep { !/^\.{1,2}$/ } readdir (DIR);
	closedir (DIR);
	for my $path (@files) {
		if (-d $path && $path =~ /\/(bundles|themes)(\/|$)/) {
			migrateDir($path);
			if ($path =~ /\.itBundle$/ && !-e $path."\/info.itInfo") {
				migrateBundle($path."\/info.itInfo");
			}
		} else {
			#print $path."\n" if $path =~ /\.it\w+$/;
			migrateBundle($path) if $path =~ /\.it\w+$/;
		}
	}
}

sub updateBundleMenu {
	my $path = shift;
	opendir (DIR, $path) or die "Unable to open $path: $!";
	my @files = map { $path . '/' . $_ } grep { !/^\.{1,2}$/ } readdir (DIR);
	closedir (DIR);
	for my $path (@files) {
		if (-d $path && $path =~ /\/(bundles|themes)(\/|$)/) {
			updateBundleMenu($path);
			if ($path =~ /\.itBundle$/ && !-e $path."\/info.itInfo") {
				migrateBundle($path."\/info.itInfo");
			}
		} else {
			if ($path =~ /\/info.itInfo$/) {
				print "UPDATE BUNDLE MENU: ".$path."\n";
				updateMenu($path);
			}
		}
	}
}

sub updateMenu {
	my $path = shift;
	if (open(FILE,"$path")) {
		my $string = join "", <FILE>;
		close FILE;

		my @Items = (($string =~ /\{\s*item\s*:\s*".+?"\s*\}/g), ($string =~ /\{\s*item\s*:\s*'.+?'\s*\}/g));

		my %dirconvert = ();
		$dirconvert{'snippet'} = "snippets/";
		$dirconvert{'language'} = "grammars/";
		my %extconvert = ();
		$extconvert{'snippet'} = ".itSnippet";
		$extconvert{'language'} = ".itGrammar";

		foreach my $item (@Items) {
			if ($item =~ /\{\s*item\s*:\s*('|")(.*?)\1\s*\}/) {
				my $key = $2;
				chomp($key);
				my @struct = split(/\./,$key);
				my $itempath = $path;
				$itempath =~ s/info\.itInfo$//;
				$itempath .= $dirconvert{$struct[2]};
				$itempath .= lc($struct[3]);
				$itempath .= $extconvert{$struct[2]};
				print "+ Item ".$key.":";
				if (-e $itempath) {
					if (open(ITEMFILE, $itempath)) {
						my $itemcontent = join "", <ITEMFILE>;
						close (ITEMFILE);
						if ($itemcontent =~ /uuid\s*:\s*('|")(.*?)\1/) {
							my $uid = $2;
							chomp($uid);
							$string =~ s/('|")\s*\Q$key\E\s*\1/$1$uid$1/g;
							print "$uid";
						}
					}
				}
				print "\n";
			}
		}

		if (open(FILE,">$path")) {
			print FILE $string;
			close FILE;
		}
	}
}

#~ if (open(FILE,"all_hallows_eve.itTheme")) {
#~ $c = checkEncoding("case_statement.itSnippet");
#~ $c = checkEncoding("C.migItGrammar");
#~ print $c."\n";
#~ my %Jasmin = parseHash($c);
#~ print writeJasminHash(\%Jasmin);

sub migrateBundle{
	my $file = shift;
	my $originalfile = $file;
	logit("START: ".$file);
	$processed++;
	print $file." .";
	my $c = checkEncoding($file);
	my %Jasmin = parseHash($c);
	if (scalar(keys %Jasmin) == 0) {
		if ($file =~ /info\.itInfo$/) {
			$Jasmin{'menu'} = [];
		} else {
			logit("** ERROR JASMIN PARSING FAIL on :".$file);
			print " PARSING FAIL\n";
			push(@Errors,$file);
			return;
		}
	}
	print ".";

	$file =~ s/^\Q$sourceDir/$targetDir/i;
	$file =~ s/^\Q$targetDir\/themes/$targetDir\/bundles\/Intype.itBundle\/themes/i;
	$file =~ s/\/syntaxes\//\/grammars\//i;

	if ($file =~ /([^\/]*[A-Z][^\/]*)(\.it[^\/]+)/) {
		$file = lc($file);
		$file =~ s/\.ittheme/.itTheme/g;
		$file =~ s/\.itbundle/.itBundle/g;
		$file =~ s/\.itsnippet/.itSnippet/g;
		$file =~ s/\.itgrammar/.itGrammar/g;
		$file =~ s/\.itinfo/.itInfo/g;
		logit("** LOWERCASE FILENAME:".$file);
	}
	if (!$Jasmin{"uuid"}) {
		$Jasmin{"uuid"} = "'".getUUID()."'";
		logit("** ADD UUID:".$Jasmin{"uuid"});
	}
	my $curuuid = $Jasmin{"uuid"};
	$curuuid =~ s/'//g;
	$file =~ s/^(.*)(\.it[^\/]+.*)$/$1.$curuuid$2/;
	logit("** ADD UUID TO FILENAME:".$Jasmin{"uuid"});
	delete $Jasmin{"uuid"};
	logit("** REMOVE UUID FROM FILE CONTENT");

	if (!$Jasmin{"title"}) {
		if ($originalfile =~ /([^\/]+?)\.itBundle\/info\.itInfo$/) {
			$Jasmin{"title"} = "'$1'";
		} else {
			if ($Jasmin{"name"}) {
				$Jasmin{"title"} = $Jasmin{"name"};
			} else {
				my $newtitle = $originalfile;
				$newtitle =~ s/\.it[^\/]+$//;
				$newtitle =~ s/^.*\/([^\/]+)$/$1/;
				$newtitle =~ tr/_/ /;
				$Jasmin{"title"} = "'$newtitle'";
			}
		}
		logit("** ADD TITLE:".$Jasmin{"title"});
	}
	if ($Jasmin{"scope_name"}) {
		$Jasmin{"name"} = $Jasmin{"scope_name"};
		delete $Jasmin{"scope_name"};
		logit("** RENAME scope_name TO name:".$Jasmin{"name"});
	}
	if ($Jasmin{"key_equivalent"}) {
		$Jasmin{"shortcut"} = $Jasmin{"key_equivalent"};
		delete $Jasmin{"key_equivalent"};
		logit("** RENAME key_equivalent TO shortcut:".$Jasmin{"shortcut"});
	}

	if ($file =~ /\.itGrammar$/) {
		delete $Jasmin{"shortcut"};
		logit("** REMOVE shortcut for grammar.");
	}

	print ".";
	#~ print writeJasminHash(\%Jasmin);
	checkFileDir($file);
	my $result = addUTF8BOM(writeJasminHash(\%Jasmin));
	if (open(FILE,">$file")) {
		print FILE $result;
	}
	close(FILE);
	print " OK\n";
}

sub parseArray {
	my $s = shift;
	my @Jasmin = ();
	if ($s =~ /\s*\[(.+)\]\s*/s) {
		push(@Jasmin,parseArrayNext($1));
	}
	return @Jasmin;
}

sub parseArrayNext {
	my $inside = shift;
	#~ print $inside;
	my @Jasmin = ();

	if ($inside =~ /^\s*(\/\*.*?\*\/|\/\/.*?(?>\n))/s) {
		#~ print "\nmatches A - 1:".$&."\n";
		my $curso = $+[0];
		my $comment = $1;
		chomp($comment);
		push(@Jasmin, $comment);
		push(@Jasmin,parseArrayNext(substr($inside,$curso)));
	} else {
		if ($inside =~ /^\s*/s) {
			my $curso = $+[0];
			my $next = substr($inside,$curso);
			if ($next =~ /^("|')/s){
				my $valueCurso = findEnd(substr($next,1),$1,$EscapeChar{$1});
				my $value = substr($next,0,$valueCurso+2);
				my $comment = substr($inside,$valueCurso+$curso+2);
				if ($comment =~ /^\s*(\/\*.*?\*\/|\/\/.*?(?>\n))/s) {
					$value .= $&;
					$valueCurso += $+[0];
				}
				chomp($value);
				push(@Jasmin,$value);
				push(@Jasmin,parseArrayNext(substr($inside,$valueCurso+$curso+2)));
			} else {
				if ($next =~ /^(\/)/s){
					my $valueCurso = findEnd(substr($next,1),$1,$EscapeChar{$1});
					my $value = substr($next,0,$valueCurso+2);
					my $regexmod = substr($inside,$valueCurso+$curso+2);
					if ($regexmod =~ /^[a-z]+/i) {
						$value .= $&;
						$valueCurso += $+[0];
					}
					my $comment = substr($inside,$valueCurso+$curso+2);
					if ($comment =~ /^\s*(\/\*.*?\*\/|\/\/.*?(?>\n))/s) {
						$value .= $&;
						$valueCurso += $+[0];
					}
					chomp($value);
					push(@Jasmin,$value);
					push(@Jasmin,parseArrayNext(substr($inside,$valueCurso+$curso+2)));
				} else {
					if ($next =~ /^\[/s){
						my $lenght = findBraceEnd(substr($next,1),'[',']');
						my $nextPosition = $curso + $lenght + 2;
						push(@Jasmin,[ parseArray(substr($inside,$curso,$lenght+2)) ]);
						push(@Jasmin,parseArrayNext(substr($inside,$nextPosition)));
					} else {
						if ($next =~ /^\{/s) {
							my $lenght = findBraceEnd(substr($next,1),'{','}');
							my $nextPosition = $curso + $lenght + 2;
							push(@Jasmin,{ parseHash(substr($inside,$curso,$lenght+2)) });
							push(@Jasmin,parseArrayNext(substr($inside,$nextPosition)));
						} else {
							if ($next =~ /^(-?\d+(\.\d+)?([eE][+-]?\d+)?)/s) {
								my $nextPosition = $curso + length($1) + 1;
								push(@Jasmin,$1);
								push(@Jasmin,parseArrayNext(substr($inside,$nextPosition)));
							} else {
								if ($next =~ /^(-+)/s) {
									my $lenght = $+[0];
									my $nextPosition = $curso + $lenght + 2;
									push(@Jasmin,$1);
									push(@Jasmin,parseArrayNext(substr($inside,$nextPosition)));
								}
							}
						}
					}
				}
			}
		}
	}
	return @Jasmin;
}

sub parseHash {
	my $s = shift;
	my %Jasmin = ();
	if ($s =~ /\s*\{(.+)\}\s*/s) {
		my %toAdd = parseHashNext($1);
		@Jasmin{keys %toAdd} = values %toAdd;

	}
	return %Jasmin;
}

sub parseHashNext {
	my $inside = shift;
	#~ print $inside;
	my %Jasmin = ();
	if ($inside =~ /^\s*(\/\*.*?\*\/|\/\/.*?(?>\n))/s) {
		#~ print "\nmatches H - 1:".$&."\n";
		my $curso = $+[0];
		my $comment = $1;
		#~ $Jasmin{$key} = $value;
		my %toAdd = parseHashNext(substr($inside,$curso));
		@Jasmin{keys %toAdd} = values %toAdd;
	}
	if ($inside =~ /^\s*([\w\-\.]+)\s*:\s*/s) {
		my $key = $1;
		my $curso = $+[0];
		my $next = substr($inside,$curso);
		if ($next =~ /^("|')/s){
			my $valueCurso = findEnd(substr($next,1),$1,$EscapeChar{$1});
			my $value = substr($next,0,$valueCurso+2);
			my $comment = substr($inside,$valueCurso+$curso+2);
			if ($comment =~ /^\s*(\/\*.*?\*\/|\/\/.*?(?>\n))/s) {
				$value .= $&;
				$valueCurso += $+[0];
			}
			chomp($value);
			$Jasmin{$key} = $value;
			my %toAdd = parseHashNext(substr($inside,$valueCurso+$curso+2));
			@Jasmin{keys %toAdd} = values %toAdd;
		} else {
			if ($next =~ /^(\/)/s){
				my $valueCurso = findEnd(substr($next,1),$1,$EscapeChar{$1});
				my $value = substr($next,0,$valueCurso+2);
				my $regexmod = substr($inside,$valueCurso+$curso+2);
				if ($regexmod =~ /^[a-z]+/i) {
					$value .= $&;
					$valueCurso += $+[0];
				}
				my $comment = substr($inside,$valueCurso+$curso+2);
				if ($comment =~ /^\s*(\/\*.*?\*\/|\/\/.*?(?>\n))/s) {
					$value .= $&;
					$valueCurso += $+[0];
				}
				chomp($value);
				$Jasmin{$key} = $value;
				my %toAdd = parseHashNext(substr($inside,$valueCurso+$curso+2));
				@Jasmin{keys %toAdd} = values %toAdd;
			} else {
				if ($next =~ /^\[/s){
					my $lenght = findBraceEnd(substr($next,1),'[',']');
					my $nextPosition = $curso + $lenght + 2;
					$Jasmin{$key} = [ parseArray(substr($inside,$curso,$lenght+2)) ];
					my %toAdd = parseHashNext(substr($inside,$nextPosition));
					@Jasmin{keys %toAdd} = values %toAdd;
				} else {
					if ($next =~ /^\{/s) {
						my $lenght = findBraceEnd(substr($next,1),'{','}');
						my $nextPosition = $curso + $lenght + 2;
						$Jasmin{$key} = { parseHash(substr($inside,$curso,$lenght+2)) };
						my %toAdd = parseHashNext(substr($inside,$nextPosition));
						@Jasmin{keys %toAdd} = values %toAdd;
					} else {
						if ($next =~ /^(-?\d+(\.\d+)?([eE][+-]?\d+)?)/s) {
							$Jasmin{$key} = $1;
							my $nextPosition = $curso + length($Jasmin{$key}) + 1;
							my %toAdd = parseHashNext(substr($inside,$nextPosition));
							@Jasmin{keys %toAdd} = values %toAdd;
						} 
					}
				}
			}
		}
	}
	return %Jasmin;
}

sub findEnd{
	my $string = shift;
	my $endchar = shift;
	my $escapechar = shift;
	my @s = split(//, $string);
	my $pos = 0;
	my $inEscape = 0;
	my $found = 0;
	while (!$found && $pos < scalar(@s)) {
		if ($inEscape) {
			$inEscape = 0;
		} else {
			if ($escapechar ne $endchar) {
				$inEscape = $s[$pos] eq $escapechar;
			} else {
				$inEscape = $s[$pos] eq $escapechar && ($pos + 1 < scalar(@s)) && $s[$pos+1] eq $endchar;
			}
			$found = !$inEscape && $s[$pos] eq $endchar;
		}
		$pos++;
	}
	$pos--;
	return $pos;
}
sub findBraceEnd{
	my $string = shift;
	my $startchar = shift;
	my $endchar = shift;
	my @s = split(//, $string);
	my $pos = 0;
	my $counter = 0;
	my $inString = 0;
	my $inSingleString = 0;
	my $inRegexp = 0;
	my $inEscape = 0;
	while ($counter >= 0 && $pos < scalar(@s)) {
		if (!$inString && !$inSingleString && !$inRegexp) {
			$counter++ if $s[$pos] eq $startchar;
			$counter-- if $s[$pos] eq $endchar;

			$inString = $pos if $s[$pos] eq '"';
			$inSingleString = $pos if $s[$pos] eq '\'';
			$inRegexp = $pos if $s[$pos] eq '/';
		} else {
			$inString = 0 if $inString && !$inEscape && $s[$pos] eq '"';
			$inSingleString = 0 if $inSingleString && $s[$pos] eq '\'';
			$inRegexp = 0 if $inRegexp && !$inEscape && $s[$pos] eq '/';
		}
		if ($inEscape) {
			$inEscape = 0;
		} else {
			$inEscape++ if ($inString || $inRegexp) && $s[$pos] eq '\\'
		}
		#~ print $pos.":".$s[$pos]."($counter)-$inString-$inSingleString-$inRegexp\n";
		$pos++;
	}
	$pos--;
	return $pos;
}

sub writeJasminArray {
	my $Jasminref = shift;
	my $ind = shift || 0;
	my $ident = $ind;
	my @Jasmin = @{$Jasminref};
	my $s;
	my $n = scalar(@Jasmin);
	#~ print ".";
	$ident++ if $n > 1;
	$s = "[";
	$s .= "\n" if $n > 1;
	$s .= " " if $n <= 1;
	foreach my $elem (@Jasmin) {
		$s .= "\t" x $ident if $n > 1;
		if(ref($elem) eq 'ARRAY'){
			$s .= writeJasminArray($elem,$ident);
		} else {
			if(ref($elem) eq 'HASH'){
				$s .= writeJasminHash($elem,$ident);
			} else {
				$s .= $elem;
			}
		}
		$s .=  "\n" if $n > 1;
	}
	$s .= "\t" x ($ident-1) if $n > 1;
	$s .= " " if $n == 1;
	$s .= "]";
	return $s;
}

sub writeJasminHash {
	my $Jasminref = shift;
	my $ind = shift || 0;
	my $ident = $ind;
	my %Jasmin = %{$Jasminref};
	my $s;
	my $n = scalar(keys %Jasmin);
	$ident++ if $n > 1;
	$s = "{";
	$s .= "\n" if $n > 1;
	$s .= " " if $n <= 1;
	foreach my $key (sort keys %Jasmin) {
		$s .= "\t" x $ident if $n > 1;
		$s .= $key.": ";
		if(ref($Jasmin{$key}) eq 'ARRAY'){
			$s .= writeJasminArray($Jasmin{$key},$ident);
		} else {
			if(ref($Jasmin{$key}) eq 'HASH'){
				$s .= writeJasminHash($Jasmin{$key},$ident);
			} else {
				$s .= $Jasmin{$key};
			}
		}
		$s .= "\n" if $n > 1;
	}
	$s .= "\t" x ($ident-1) if $n > 1;
	$s .= " " if $n == 1;
	$s .= "}";
	return $s;
}

sub getRunTime{
	my ($second, $minute, $hour, $dayOfMonth, $month, $yearOffset, $dayOfWeek, $dayOfYear, $daylightSavings) = localtime();
	my $year = 1900 + $yearOffset;
	$month++;
	$month = '0'.$month if $month < 10;
	$dayOfMonth = '0'.$dayOfMonth if $dayOfMonth < 10;
	$hour = '0'.$hour if $hour < 10;
	$minute = '0'.$minute if $minute < 10;
	$second = '0'.$second if $second < 10;
	return $year."-".$month."-".$dayOfMonth."_".$hour."-".$minute."-".$second;
}

sub init{
	if ($DEBUG) {

		$LOG_FILENAME = "bundle-convertor_$runTime.log";
		open(LOG,">>".$LOG_FILENAME);
	}
	#~if (! -e "uuid.vbs") {
	#~	if (open(VBS,">uuid.vbs")) {
	#~		print VBS "WScript.Echo(CreateObject(\"Scriptlet.TypeLib\").Guid)";
	#~		close(VBS);
	#~	}
	#~}
}

sub getUUID{
	#~ use Win32::OLE;
	#~ my $TypeLib = CreateObject Win32::OLE 'Scriptlet.TypeLib' or die $!;
	#~ my $result = $TypeLib->Guid;
	#uuid.vbs: WScript.Echo(CreateObject("Scriptlet.TypeLib").Guid)
	#my $result = `cscript //Nologo uuid.vbs`;
	#if ($result =~ /{(.*?)}/) {
	#	return $1;
	#}
	return `uuid.exe`;
}

sub destroy {
	#~if (-e "uuid.vbs") {
	#~	unlink("uuid.vbs");
	#~}
	close(LOG) if $DEBUG;
}

sub logit {
	#~ print join("\n", @_ )."\n" if $DEBUG;
	print LOG join("\n", @_ )."\n" if $DEBUG;
}


sub checkFileDir{
	my $path = shift;
	$path =~ s/\/[^\/]+$//;
	checkDir($path);
}

sub checkDir {
	my $path = shift;
	if (!-e $path) {
		if ($path =~ /(.*\/.*?)\/[^\/]+$/) {
			checkDir($1);
			mkdir($path);
		} else {
			mkdir($path);
		}
	}
}

sub checkEncoding {
	my $path = shift;
	my $encoding = identifyUnicodeWithBOM($path);
	my $winpath = $path;
	$winpath =~ s/\//\\/g;
	if ($encoding eq 'UTF-16LE') {
		`uniconv.exe Unicode20:BOM:little-endian "$winpath" Unicode20:UTF8 uniconv-out`;
		if (open(TEMPUNICONV,"uniconv-out")) {
			$string = join "", <TEMPUNICONV>;
			close(TEMPUNICONV);
			unlink("uniconv-out");
			return $string;
		}
	}
	if ($encoding eq 'UTF-16BE') {
		`uniconv.exe Unicode20:BOM:big-endian "$winpath" Unicode20:UTF8 uniconv-out`;
		if (open(TEMPUNICONV,"uniconv-out")) {
			$string = join "", <TEMPUNICONV>;
			close(TEMPUNICONV);
			unlink("uniconv-out");
			return $string;
		}
	}
	if (open(TEMPUNICONV,$path)) {
		$string = join "", <TEMPUNICONV>;
		close(TEMPUNICONV);
		return $string;
	}
	return "";
}

sub addUTF8BOM {
	$str = shift;
	if (identifyUnicodeWithBOM($str)) {
		return $str;
	} else {
		return chr(0xEF).chr(0xBB).chr(0xBF).$str;
	}
}

sub identifyUnicodeWithBOM {
	my $path = shift;
	if (open(FILE,$path)) {
		my $bom;
		read FILE, $bom, 4;
		close FILE;
		my $ord1 = ord(substr($bom,0,1));
		my $ord2 = ord(substr($bom,1,1));
		my $ord3 = ord(substr($bom,2,1));
		my $ord4 = ord(substr($bom,3,1));

		if (($ord1 == 0x00) && ($ord2 == 0x00) && ($ord3 == 0xFE) && ($ord4 == 0xFF)) {
			return "UTF-32BE";
		}
		if (($ord1 == 0xFF) && ($ord2 == 0xFE) && ($ord3 == 0x00) && ($ord4 == 0x00)) {
			return "UTF-32LE";
		}
		if (($ord1 == 0xEF) && ($ord2 == 0xBB) && ($ord3 == 0xBF)) {
			return "UTF-8";
		}
		if (($ord1 == 0xFE) && ($ord2 == 0xFF)) {
			return "UTF-16BE";
		}
		if (($ord1 == 0xFF) && ($ord2 == 0xFE)) {
			return "UTF-16LE";
		}
	}
	return "";
}
